eletrotupi / tcc/ frontend/app/entry/[id].tsx1228979c846b33d8390ecbbd7fd746acc32df67a
8.3 KB | 285 lines Raw
1
import { useState, useEffect } from 'react';
2
import { StyleSheet, Text, View, ScrollView, Pressable, ActivityIndicator } from 'react-native';
3
import { useLocalSearchParams, router, Stack } from 'expo-router';
4
import { format } from 'date-fns';
5
import { ptBR } from 'date-fns/locale';
6

7
import { Card, SubtleInfoCard } from '@/components/ui/Cards';
8
import { Badge } from '@/components/ui/Badge';
9
import { Button } from '@/components/ui/Button';
10
import { Col, Between, Row } from '@/components/ui/LayoutHelpers';
11
import { Section, SectionHeader } from '@/components/ui/Sections';
12
import { Theme, Colors, Spacing } from '@/constants/theme';
13
import { getMood } from '@/constants/moods';
14
import { getMoodComponent, intensityLabel, intensityToValue } from '@/constants/mood-components';
15
import { useMoodEntry, useDeleteMoodEntry } from '@/hooks';
16
import { getTimeBadge, formatMoment } from '@/lib/utils/time';
17
import { MoodComponentCard } from '@/components/ui/MoodComponentCard';
18

19
export default function MoodDetailScreen() {
20
  const { id } = useLocalSearchParams<{ id: string }>();
21
  const { data: entry, isLoading, isError } = useMoodEntry(id);
22
  const deleteMoodEntry = useDeleteMoodEntry();
23
  const [minutesLeft, setMinutesLeft] = useState(0);
24
  const [canDelete, setCanDelete] = useState(false);
25

26
  if (isLoading) {
27
    return (
28
      <View style={styles.centered}>
29
        <ActivityIndicator size="large" color={Theme.colors.tint} />
30
      </View>
31
    );
32
  }
33

34
  if (isError || !entry) {
35
    return (
36
      <View style={styles.centered}>
37
        <Text style={styles.errorText}>Registro não encontrado.</Text>
38
        <Pressable onPress={() => router.back()} style={styles.backButton}>
39
          <Text style={styles.backButtonText}>Voltar</Text>
40
        </Pressable>
41
      </View>
42
    );
43
  }
44

45
  const moment = new Date(entry.moment);
46
  const moodDef = getMood(entry.selectedMood.toLowerCase());
47
  const badge = getTimeBadge(moment);
48

49
  useEffect(() => {
50
    const updateTimer = () => {
51
      const moment = new Date(entry.moment);
52
      const createdAt = new Date(entry.moment);
53
      const msSinceCreation = Date.now() - createdAt.getTime();
54
      const deletionWindowMs = 5 * 60 * 1000;
55

56
      const newCanDelete = msSinceCreation < deletionWindowMs;
57
      const newMinutesLeft = Math.max(0, Math.ceil((deletionWindowMs - msSinceCreation) / 60000));
58

59
      setCanDelete(newCanDelete);
60
      setMinutesLeft(newMinutesLeft);
61
    };
62

63
    updateTimer(); // Set initial values
64

65
    const interval = setInterval(updateTimer, 1000);
66

67
    return () => clearInterval(interval);
68
  }, [entry.moment]);
69

70

71
  const handleDelete = async () => {
72
    await deleteMoodEntry.mutateAsync(String(entry.id));
73

74
    router.back();
75
  };
76

77
  const deleteButtonMsg = () => {
78
    const msg = "Excluir Registro";
79
    const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? '<1' : minutesLeft}:00)`;
80
    const suffix = canDelete ? timeLeftStr : "(Expirado)"
81

82
    return `${msg} ${suffix}`
83
  }
84

85
  return (
86
    <>
87
      <Stack.Screen options={{ title: 'Ver detalhes' }} />
88

89
      <ScrollView
90
        style={styles.container}
91
        contentContainerStyle={styles.content}
92
        showsVerticalScrollIndicator={false}
93
      >
94
        {/* Header card */}
95
        <Card style={styles.headerCard}>
96
          <Between>
97
            <Row gap={12} style={{ alignItems: 'center' }}>
98
              <Text style={styles.moodIcon}>{moodDef?.icon ?? '😐'}</Text>
99
              <Col gap={2}>
100
                <Text style={styles.moodLabel}>{moodDef?.label ?? entry.selectedMood}</Text>
101
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
102
              </Col>
103
            </Row>
104
            <Badge
105
              label="Registro Ativo"
106
              backgroundColor={Theme.colors.activeBackground}
107
              color={Theme.colors.tint}
108
            />
109
          </Between>
110
        </Card>
111

112
        {/* Core levels */}
113
        <Section>
114
          <SectionHeader title="Níveis" variant="subtle" />
115
            {[
116
              { label: 'Energia',  value: entry.energyLevel  },
117
              { label: 'Estresse', value: entry.stressLevel  },
118
              { label: 'Ansiedade', value: entry.anxietyLevel },
119
            ].map((item, index, arr) => (
120
              <Card style={{ padding: Spacing.cardGap }}>
121
                <View key={item.label}>
122
                  <View style={styles.componentRow}>
123
                    <Text style={styles.componentLabel}>{item.label}</Text>
124
                    <Text style={styles.componentIntensityGreen}>{item.value}/10</Text>
125
                  </View>
126

127
                  <View style={styles.progressTrack}>
128
                    <View style={[styles.progressFill, { width: `${(item.value / 10) * 100}%` }]} />
129
                  </View>
130
                </View>
131
              </Card>
132
            ))}
133
        </Section>
134

135
        <Section>
136
          <SectionHeader title="Componentes" variant="subtle" />
137
          <Col gap={0}>
138
            {entry.moodComponents.map((comp, index) => {
139
              const def = getMoodComponent(comp.component.toLowerCase());
140
              const numericIntensity = comp.intensity === 'LIGHT' ? 2 : comp.intensity === 'MODERATE' ? 5 : 8;
141

142
              return (
143
                <MoodComponentCard
144
                  key={comp.id}
145
                  id={comp.component.toLowerCase()}
146
                  intensity={comp.intensity}
147
                />
148
              );
149
            })}
150
          </Col>
151
        </Section>
152

153
        {/* Annotation / notes */}
154
        {entry.annotation && (
155
          <Section>
156
            <SectionHeader title="Notas" variant="subtle" />
157
            <Card style={{ padding: Spacing.cardGap }}>
158
              <Text style={styles.annotation}>"{entry.annotation?.trim()}"</Text>
159
            </Card>
160
          </Section>
161
        )}
162

163
        {/* Delete */}
164
        <View style={styles.section}>
165
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
166
          <Button variant="danger"
167
            isLoading={deleteMoodEntry.isPending}
168
            disabled={!canDelete || deleteMoodEntry.isPending}
169
            onPress={handleDelete}
170
            title={deleteButtonMsg()} />
171

172
          <SubtleInfoCard style={styles.deleteInfo}
173
            text={`
174
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
175
              após o envio para garantir a integridade do histórico emocional.
176
              `} />
177
        </View>
178

179
      </ScrollView>
180
    </>
181
  );
182
}
183

184
const styles = StyleSheet.create({
185
  container: {
186
    flex: 1,
187
    backgroundColor: Theme.colors.background,
188
  },
189
  content: {
190
    padding: Spacing.containerPadding,
191
    gap: Spacing.sectionGap,
192
    paddingBottom: 48,
193
  },
194
  centered: {
195
    flex: 1,
196
    justifyContent: 'center',
197
    alignItems: 'center',
198
    gap: 12,
199
  },
200
  errorText: {
201
    fontSize: 15,
202
    color: Colors.light.textSecondary,
203
  },
204
  backButton: {
205
    paddingHorizontal: 20,
206
    paddingVertical: 10,
207
    backgroundColor: Theme.colors.tint,
208
    borderRadius: Theme.borderRadius.md,
209
  },
210
  backButtonText: {
211
    fontWeight: '700',
212
    color: Theme.colors.text,
213
  },
214

215
  // Header
216
  headerCard: {
217
    padding: 16,
218
  },
219
  moodIcon: {
220
    fontSize: 40,
221
  },
222
  moodLabel: {
223
    fontSize: 17,
224
    fontWeight: '700',
225
    color: Colors.light.text,
226
  },
227
  moodTime: {
228
    fontSize: 13,
229
    color: Colors.light.textSecondary,
230
  },
231

232
  // Sections
233
  section: {
234
    gap: 8,
235
  },
236
  sectionLabel: {
237
    fontSize: 11,
238
    fontWeight: '600',
239
    letterSpacing: 0.8,
240
    color: Colors.light.textSecondary,
241
    textTransform: 'uppercase',
242
  },
243

244
  // Components
245
  componentRow: {
246
    flexDirection: 'row',
247
    justifyContent: 'space-between',
248
    alignItems: 'center',
249
    paddingVertical: 10,
250
  },
251
  componentLabel: {
252
    fontSize: 15,
253
    fontWeight: '700',
254
    color: Colors.light.text,
255
  },
256
  componentIntensity: {
257
    fontSize: 13,
258
    color: Colors.light.textSecondary,
259
  },
260
  componentIntensityGreen: {
261
    fontSize: 13,
262
    fontWeight: '700',
263
    color: Theme.colors.tint,
264
  },
265
  progressTrack: {
266
    height: 6,
267
    backgroundColor: Theme.colors.light.divider,
268
    borderRadius: Theme.borderRadius.full,
269
    marginBottom: 4,
270
    overflow: 'hidden',
271
  },
272
  progressFill: {
273
    height: '100%',
274
    backgroundColor: Theme.colors.light.tint,
275
    borderRadius: Theme.borderRadius.full,
276
  },
277
  // Annotation
278
  annotation: {
279
    fontSize: 14,
280
    lineHeight: 22,
281
    color: Colors.light.textSecondary,
282
    fontStyle: 'italic',
283
    padding: 4,
284
  },
285
});